///////////////
// CONSTANTS //
///////////////

const RUN_TESTS = false;
const CALCULATOR_LOADED = true;
const CALCULATOR_NAME = "Calculator";
const CALCULATOR_INPUT = "calc_input";
const UPDATED_URL = "http://code.google.com/p/firefoxcalculator/wiki/Calculator_Updated";
const WINDOW = "window";
const BASE = 10;
const PRECISION = 12;

//////////////////////////
// Class implementation //
//////////////////////////

function Calculator() {
  this.init();
}

Calculator.prototype = {
  result: null,
  calcHistory: null,
  functions: null,
  mBranch: null,

  init: function() {
    try {
      // load the preferences
      var prefService = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService);
      this.mBranch = prefService.getBranch("calculator.");

      // MESSY, this test needs fixing!
      // show release notes if the extension has been updated
      // set pref to show release notes if the extension has been used before
      try {
        var val = eval("this.mBranch.getIntPref('" + PREF_NAME_ANGLE + "')");
        this.getPreference(PREF_UPDATED, "Int", 1);
      } catch (e) {}

      if (this.getPreference(PREF_UPDATED, "Int", 0) == 1 && 0) {
        this.setPreference(PREF_UPDATED, "Int", 0);
        this.openTab(UPDATED_URL);
      }

      // load the main modules
      this.calcHistory = new History(this);
      this.functions = new Functions(this);
      var input = document.getElementById(CALCULATOR_INPUT);
      input.focus();

      // Manage UI options
      var aButton = document.getElementById("show-hide-button");
      if (this.getPreference(PREF_NAME_CALC_PAD, "Int") == PAD_YES) {
        document.getElementById("calculator-buttons").hidden = false;
        aButton.image = "chrome://calculator/skin/hide.png";
      } else {
        document.getElementById("calculator-buttons").hidden = true;
        aButton.image = "chrome://calculator/skin/show.png";
      }

      // run tests
      if (RUN_TESTS)
        this.tests();
    } catch (e) {
      alert(e);
    }
  },

  close: function() {},
  /////////////////////////
  // Setters and getters //
  /////////////////////////

  setValue: function(id, value) {
    try {
      document.getElementById(id).value = value;
    } catch (e) {}
  },


  getValue: function(id) {
    try {
      return document.getElementById(id).value;
    } catch (e) {}
    return null;
  },
  getPreference: function(aPref, aType, aDefault) {
    try {
      var val = eval("this.mBranch.get" + aType + "Pref('" + aPref + "')");
      return val;
    } catch (e) {
      this.setPreference(aPref, aType, aDefault);
      return aDefault;
    }
    return null;
  },
  setPreference: function(aPref, aType, aValue) {
    try {
      eval("this.mBranch.set" + aType + "Pref('" + aPref + "'," + aValue + ")");
      return true;
    } catch (e) {
      alert(e);
    }
    return false;
  },


  //////////////
  // Commands //
  //////////////
  enter: function() {
    if (this.getValue(CALCULATOR_INPUT) != "") {
      this.calcHistory.addExpression(this.getValue(CALCULATOR_INPUT), this.evaluate(this.getValue(CALCULATOR_INPUT)));
    }
    this.reset();
    //     var input = document.getElementById(CALCULATOR_INPUT);
    //     input.focus();
  },

  reset: function() {
    this.setValue(CALCULATOR_INPUT, "");
    var input = document.getElementById(CALCULATOR_INPUT);
    input.focus();
  },

  exit: function() {
    window.close();
  },
  save: function() {},
  help: function() {
    window.open("help.xul", "Help", "chrome");
  },
  about: function() {
    window.openDialog("chrome://calculator/content/about.xul", "About", "chrome");
  },
  showKeys: function() {
    var element = document.getElementById("calculator-buttons");
    var aButton = document.getElementById("show-hide-button");
    this.calcHistory.scroll();
    element.hidden = !element.hidden;
    if (element.hidden) {
      aButton.image = "chrome://calculator/skin/show.png";
      this.setPreference(PREF_NAME_CALC_PAD, "Int", PAD_NO);
    } else {
      aButton.image = "chrome://calculator/skin/hide.png";
      this.setPreference(PREF_NAME_CALC_PAD, "Int", PAD_YES);
    }
  },
  openTab: function(url) {
    try {
      return Components
        .classes['@mozilla.org/appshell/window-mediator;1']
        .getService(Components.interfaces.nsIWindowMediator)
        .getMostRecentWindow('navigator:browser', true)
        .delayedOpenTab(url, null, null, null, null);
    } catch (e) {
      return window.open(url, "_blank") != null;
    }

  },


  ///////////////////////
  // Interface related //
  ///////////////////////
  addToInput: function(func) {
    var input = document.getElementById(CALCULATOR_INPUT);
    var str = input.value;
    var curspos = input.selectionEnd;
    var res = "";
    var newPos = curspos + func.length;
    if (func.indexOf("(") != -1) {
      func += ")";
    }
    if (this.getPreference(PREF_NAME_FORMAT, "Int", FORMAT_EN) == FORMAT_FR && 0) {
      func = func.replace(new RegExp(',', "gi"), ';');
      func = func.replace(new RegExp('\\.', "gi"), ',');
    }
    res = res.concat(str.slice(0, curspos), func, str.slice(curspos, str.length));
    input.value = res;
    input.setSelectionRange(newPos, newPos);
    input.focus();
  },
  backspace: function() {
    var input = document.getElementById(CALCULATOR_INPUT);
    input.value = input.value.slice(0, input.value.length - 1);
    input.focus();
  },

  checkInput: function() {
    var input = document.getElementById(CALCULATOR_INPUT);
    var str = input.value;
    if (str.length == 1) {
      var c = str[0];
      if ((c == '/' || c == '*' || c == '+' || c == '%' || c == '^') && this.calcHistory.index != 0) {
        this.calcHistory.current_pos = this.calcHistory.index - 1;
        this.calcHistory.setInputByKey();
        input.value = input.value.concat(c);
      }
    }
  },

  ///////////////////
  // Core business //
  ///////////////////
  basicEval: function(expr) {
    try {
      var re = new RegExp('__conv__', "gi");
      if (re.test(expr)) {
        return expr;
      }

      var num = new Number(eval(expr));
      num = num.toFixed(PRECISION);
      num = num.toString(BASE);
      if (num == "NaN") {
        num = 0;
      }
      return eval(num);
      // return eval(num+0); // fix for a js bug (deprecated?)
    } catch (e) {
      throw WRONG;
    }
  },

  findFunction: function(expr) {
    var i;
    var j;
    for (i = 0; i < expr.length; i++) {
      var c = expr[i];
      // Find functions
      if (!((c >= '0' && c <= '9') ||
        c == '+' || c == '-' ||
        c == '*' || c == '/' || c == '.' ||
        c == '(' || c == ')' || c == '%')) {
        // if it's a base > 10
        for (j = i; j < expr.length; j++) {
          c = expr[j];
          if (!((c >= 'a' && c <= 'z') ||
            (c >= '0' && c <= '9'))) {
            break;
          }
        }
        if (c != '(')
          continue;

        // Extract the sub expression
        var subexpr = this.getSubExpr(expr, i);

        // Select the function and calculate

        var res = this.functions.selectFunction(subexpr);
        if (res == WRONG_FUNC) {
          throw WRONG_FUNC;
        }

        // put the calculated value into expr
        var extra = "";
        if (i > 0 && expr[i - 1].match(/[0-9]/))
          extra = "*"
        expr = this.replaceSubstring(expr, subexpr, extra + res);
      }
    }
    return this.basicEval(expr);
  },

  // This is kinda the main thing...
  evaluate: function(expr) {
    try {
      // Cleaning and replacing
      expr = this.formatStylePassIn(expr);
      expr = expr.replace(/([.0]*)([0-9]+)/gi, function(s, a, b) {
        return (a.indexOf(".") != -1 ? a.replace(/^0+/, "0") : "") + b;
      });
      expr = this.constantPass(expr);
      expr = expr.toLowerCase();
      expr = this.signPass(expr);
      // The real important line...
      var res = this.formatStylePassOut(this.findFunction(expr));
      var re = /^(-?)([0-9]{4,})([0-9.]*)$/;
      if (res.match(re) && this.getPreference(PREF_NAME_READABILITY, "Int", 0) == 0)
        return res.replace(re, function(str, a, b, c) {
          return a + (function(x) {
            var out = "";
            var i = 0,
              j = 0;
            for (i = x.length - 1; i >= 0; i--) {
              out = x.charAt(i) + out
              if (j++ % 3 == 2 && i != 0) out = " " + out;
            }
            return out;
          })(b) + c;
        });
      return res;
    } catch (e) {
      return e;
    }
    return WRONG;
  },

  formatStylePassIn: function(expr) {
    if (expr.match(new RegExp(".*?[*/]\\s*\\d+\\s*%\\s*", "gi")))
      expr = expr.replace(new RegExp('%', "gi"), '/100');
    else if (expr.indexOf("%") != -1) {
      expr = expr.replace(
        new RegExp("(.*?)([+-])\\s*(\\d+)\\s*%(.*?)", "gi"),
        function(all, left, operator, number, right) {
          return "" + left + operator + (number / 100) + "*(" + left + ")" + right;
        });
    }
    expr = expr.replace(new RegExp('mod', "gi"), '%');
    expr = expr.replace(new RegExp(':.*', "gi"), '');
    expr = expr.replace(new RegExp('//.*', "gi"), '');
    expr = expr.replace(new RegExp(' ', "gi"), '');
    if (this.getPreference(PREF_NAME_FORMAT, "Int", FORMAT_EN) == FORMAT_FR) {
      expr = expr.replace(new RegExp(',', "gi"), '.');
      expr = expr.replace(new RegExp(';', "gi"), ',');
    }
    return expr;
  },

  formatStylePassOut: function(expr) {
    var re = new RegExp('__conv__', "gi");
    if (re.test(expr)) {
      expr = expr.replace(new RegExp('__conv__', "gi"), "");
      return expr;
    }

    // fix for a javascrit bug (deprecated?)
    // round

    // expr = Math.round(expr*10000000000)/10000000000;
    //         // if length of precision is 12 cut one !
    //         var str = expr + "";
    //     var re = /\-?(\d+)\.?(\d*)/i;
    //         var found = str.match(re);
    //         if (found != null && found.length == 3 &&
    //          found[2].length > 11) {
    //           expr = Number(str.substr(0, str.length - 1));
    //         }

    expr += "";
    // decimal sep thing
    if (this.getPreference(PREF_NAME_FORMAT, "Int", FORMAT_EN) == FORMAT_FR) {
      expr = expr.replace(new RegExp(',', "gi"), ';');
      expr = expr.replace(new RegExp('\\.', "gi"), ',');
    }
    return expr;
  },

  constantPass: function(expr) {
    expr = expr.replace(/([0-9])(PI|LN|LOG|SQRT)/gi, function(str, a, b) {
      return a + "*" + b;
    });
    expr = expr.replace(new RegExp('LOG10E', "gi"), Math.LOG10E.toString(BASE));
    expr = expr.replace(new RegExp('LOG2E', "gi"), Math.LOG2E.toString(BASE));
    if (!expr.match(/base16to/gi))
      expr = expr.replace(new RegExp('E', "g"), Math.E.toString(BASE));
    expr = expr.replace(new RegExp('LN10', "gi"), Math.LN10.toString(BASE));
    expr = expr.replace(new RegExp('LN2', "gi"), Math.LN2.toString(BASE));
    expr = expr.replace(new RegExp('PI', "gi"), Math.PI.toString(BASE));
    expr = expr.replace(new RegExp('SQRT1/2', "gi"), Math.SQRT1_2.toString(BASE));
    expr = expr.replace(new RegExp('SQRT2', "gi"), Math.SQRT2.toString(BASE));
    return expr;
  },

  // Turns ! into fact() and ^ into pow(,)
  signPass: function(expr) {
    var re = new RegExp('!(-?[0-9.]+)', "gi");
    if (re.test(expr)) {
      expr = expr.replace(re, 'fact($1)');
    }
    re = RegExp("!", "gi");
    if (re.test(expr)) {
      expr = expr.replace(re, 'fact');
    }
    // Handle "^"
    // Find the character ^
    var pos = 0;
    if ((pos = expr.indexOf("^")) == -1) {
      return expr;
    }
    // Get the thing on the right
    var right = "";
    // if the first char is (, find the closing element
    var c = expr[pos + 1];
    if (c == '(' || (c >= 'a' && c <= 'z')) {
      right = this.getSubExpr(expr, pos + 1);

      // else read [\-0-9\.]+
    } else {
      var i = pos + 1;
      for (i; i < expr.length; i++) {
        if (!((i == pos + 1 && expr[i] == '-') ||
          expr[i] == '.' || ((expr[i] >= '0' && expr[i] <= '9'))))
          break;
      }
      if (i == pos + 1) {
        throw WRONG;
      }
      right = expr.substring(pos + 1, i);
    }
    //alert(right);

    // Get the thing on the left
    var left = "";
    // if the first char is ), find the opening element
    if (expr[pos - 1] == ')') {
      var i = pos - 1;
      var par = 0;
      for (i; i >= 0; i--) {
        if (expr[i] == ')')
          par--;
        else if (expr[i] == '(')
          par++;
        if (par == 0)
          break;
      }
      if (par != 0)
        throw WRONG;
      for (i; i >= 0; i--) {
        if (!((expr[i] >= 'a' && expr[i] <= 'z') ||
          (expr[i] >= '0' && expr[i] <= '9')))
          break;
      }
      left = expr.substring(i, pos);
      // else read [0-9\.]+
    } else {
      var i = pos - 1;
      for (i; i >= 0; i--) {
        if (!(expr[i] == '.' || ((expr[i] >= '0' && expr[i] <= '9'))))
          break;
      }
      left = expr.substring(i + 1, pos);
    }
    //alert(left);
    // Replace in expr
    expr = this.replaceSubstring(expr, left + "^" + right, "pow(" + left + "," + right + ")");
    //alert(expr);
    return this.signPass(expr);
  },

  ///////////////
  // Utilities //
  ///////////////

  getSubExpr: function(expr, pos) {
    var par = -1;
    var i;
    for (i = pos; i < expr.length; i++) {
      if (expr[i] == '(') {
        if (par == -1) {
          par = 0;
        }
        par++;
      } else if (expr[i] == ')') {
        par--;
      }
      if (par == 0) {
        break;
      }
    }
    if (par != 0) {
      throw WRONG_PAR;
    }
    return expr.substring(pos, i + 1);
  },

  replaceSubstring: function(inputString, fromString, toString) {
    return inputString.split(fromString).join(toString);
  },

  tests: function() {
    var t = Array(["1+1", "2"], ["Base16To(10,c)", "12"],
      // ["3262.05+22953.69", "26215.74"],
      ["4^(2+2)", "256"], ["2+!5+1", "123"], ["12345679*81", "999 999 999"], ["2sin(PI/2)", "2"], ["1002/3+1", "335"],

      // 16-9-07
      ["1+log10(5+3)", "1.903089986992"], ["7 mod 3 + 1", "2"], ["700*10%", "70"], ["Base2To(10,010)", "2"], ["28+2-15%", "25.5"],

      // 11-11-07
      ["fact(0)", "1"], ["sin(0)", "0"], ["001+.00200100+0.0012+00.0012+1200+0+0091+01",
       "1 293.004401"]
    );
    // var t = Array(["sin(0)", "0"]);
    var n = 0;
    var errors = "";
    for (var i in t) {
      var res = this.evaluate(t[i][0]);
      if (res != t[i][1]) {
        errors += t[i][0] + " = " + res + "\n";
        n += 1;
      }
    }
    // needs firebug!
    try {
      console.debug(t.length + " tests,  " + n + " error(s)\n" + errors);
    } catch (e) {;
    }
  }
}
